<?php

/*
Исходная постановка от Цветкова, 23 янв 08

1. показывается 1-N записей первого уровня
2. показывается все записи второго уровня у первой записи первого уровня
2.1. если количество всех записей не превысило M - перейти к следующей записи первого уровня и повторить п.2
2.2. если количество записей превысило M - выйти
3. до тех пор, пока общее количество записей не превысит M, поочерёдно показывать записи третьего уровня
четвёртый уровень не вгружать
* четвёртый и далее

*/

class Model_TreeComments extends RL_Model2
{
	private $comments_count = 0; 	// тут будем хранить общее кол-во комментариев
	private $level = 1; 			// это относительный уровень, те не относительно корневого эл-та.
	// нужен для работы рекурсивного алгоритма
	// те начинаем с 1-го уровня элемента parent_oid
	private $all_comments = array();
	private $parent_oid;
	private $not_loaded = array(); 	// идшники родителей, у которых не загружены дети, это для подгрузчика 1-го типа (см. ниже)
	private $level_one_offset;

	/**
	 * Устанавливает параметры отдачи комментариев. N - сколько разварачивать на первом уровне,
	 * M - сколько развоварачивать всего в рамках одного запроса(включая первый уровень).
	 * $ordering - сортировка в рамках одного уровня по времени создания
	 *
	 * @return void
	 * @param int $n
	 * @param int $m
	 * @param string $ordering, или asc или desc
	 * @author Alexander Sibiryakov
	 **/
	public function setTCparams($n=NULL, $m=NULL, $ordering="asc") {
		define("N", isset($n) ? $n : 20);
		define("M", isset($m) ? $m : 50);
		define("ORDERING", ($ordering=="desc") ? "DESC" : "ASC");
	}

	/**
	 * Получить комментарии и листалку 2-го типа. На входе $parent_oid - идшника записи или комментария,
	 * $item_oid - идшник записи, $offset - смещение относительно уровня элемента $parent_oid
	 *
	 * @return void
	 * @author Alexander Sibiryakov
	 **/
	public function getComments( $parent_oid, $item_oid, $offset=NULL ) {

		/* план примерно такой
		1. Выбираем N комментариев первого уровня и к каждому кол-во детей на втором уровне
		2. Вычисляем как кол-во детей на втором уровне соотносится с M, и создаем список комментов первого уровня к которым запросим второй.
		3. Делаем запрос второго уровня, непременно с инфой о том сколько комментов остается на втором уровне.
		4. Если запрос был с оценкой, то решаем какие именно комменты выбирать. И делаем выборку комментов 3-го уровня.
		5. Делаем сортировку и вывод.

		те Логика системы такова что она выведет N комментов первого уровня, если их больше то для них будет подгрузчик 2-го типа.
		Дальше она выведет 2-й уровень(а потом и 3-й) до тех пор пока суммарное кол-во комментов не превысит M,
		тем родителям чьи дети не вывелись из за превышения M, выведется подгрузчик 1-го уровня.

		Подгрузка страницы.
		Подгрузка бывает двух видов:
		1) уровня/ветви - в этом случае на входе передается только ид родителя, дальше метод руководствуется константами N и M
		2) части уровня - на входе ид родителя и смещение в виде порядкового номера, метод также руководствуется N и M

		Подгрузка 2-го типа возникает только в том случае если на первом уровне запрашиваемого родителя существует
		больше N комментариев.

		*/
		$this->parent_oid = $parent_oid;

		if (isset($offset)) $this->level_one_offset = $offset;
		if (($parent_oid == $item_oid) && !isset($offset)) $this->first_load = true;

		// расчет всего для листалки второго типа
		$level_one_count = $this->getCommentsCountLevelOne( $this->parent_oid );

		// кол-во записей которые мы не сможем загрузить из первого уровня из-за ограничений по N
		// относительно элемента parent_oid
		if (isset($this->level_one_offset)) $level_one_notloaded = $level_one_count - $this->level_one_offset - N;
		else $level_one_notloaded = $level_one_count - N;


		if ($level_one_notloaded > 0) {

			$loader_type2 = array(
			"parent_oid"	=> $this->parent_oid,
			"l_count"		=> $level_one_notloaded,
			"offset"		=> N + $this->level_one_offset,
			"view_level"	=> $item_oid==$parent_oid ? 0 : $this->acl_ass->getCommentLevel( $item_oid, $this->parent_oid ), // те 1-й подуровень parent_oid но отсчитанный от корневого узла
			);
		}

		// сами запросы
		$this->getComments_Recursive(N);

		// уточняет кол-во комментов для подгрузчиков
		$this->getLoaderCounts();

		$this->all_comments = $this->sortCommentsByParent( $this->all_comments );

		// отправляет отсортированные
		return array(
		$this->decOffset($this->all_comments),
		$loader_type2
		);
	}

	private function decOffset($a) {
		foreach ($a as $k=>$v) {
			$a[$k]['offset']--;
		}
		return $a;
	}

	private function getLoaderCounts() {
		// этот метод собирает в массив все комментарии для которых нужно подсчитать кол-во подкомментариев,
		// затем делает прецельный запрос именно для этих узлов

		foreach ($this->all_comments as $k=>$v) {
			if (isset($this->not_loaded[ $v['oid'] ]) && $v['l_count'] > 0) {
				$this->all_comments[ $k ]['have_more'] = 1;
				$this->all_comments[ $k ]['loader_level_offset'] = $this->all_comments[ $k ]['offset'];

				$needs_count_oids[] = "parent.id_object = ".$v['oid'];
			}
		}

		if (count($needs_count_oids) > 0) {
			$sql_oids_enum = implode(" OR ", $needs_count_oids);
			$sql = "
			SELECT parent.id_object, COUNT(parent.id) as cnt
			FROM t_acl_tree AS node, t_acl_tree AS parent
			WHERE (parent.lft < node.lft AND parent.rgt > node.rgt) AND (".$sql_oids_enum.")
			GROUP BY parent.id_object";

			$count = $this->db->Query($sql);
			foreach ($count as $k=>$v) {
				$c_counts[ $v['id_object'] ] = $v['cnt'];
			}

			foreach ($this->all_comments as $k=>$v) {
				if (isset($c_counts[$v['oid']])) $this->all_comments[$k]['l_count'] = $c_counts[$v['oid']];
			}
		}

	}

	private function getComments_Recursive($count=NULL, array $comm_oids=NULL) {

		if (isset($_GET['debug_ajax'])) $debug = true; // включает вывод отладочной информации

		if ($debug) echo "1<br/>";
		//		echo "comm_oids count: ".count($comm_oids)."<br />";

		if (($this->first_load) && ($this->level > 3)) {
			// условие о том, что дальше третьего уровня не вгружать
			if ($debug) echo "stop loading after 3 levels<br/>";

			// проставим всем комментариям-родителям флаг о том что детей придется подгружать
			foreach ($comm_oids as $oid) { $this->not_loaded[$oid] = 1; }
			return; // выходим из рекурсии
		}


		if ($debug) echo "getting level ".$this->level."<br/>";
		if (isset($count)) {
			list($comments, $next_level_c_count) = $this->getCommentsOnLevel($this->parent_oid, $this->level, $this->level_one_offset ? $this->level_one_offset : 0, $count);
		} else {
			list($comments, $next_level_c_count) = $this->getCommentsOnLevel($this->parent_oid, $this->level, NULL, NULL, $comm_oids);
		}

		if (count($comments) == 0) return; // ничего не получили на уровне - делаем выход

		$this->comments_count += count($comments);
		if ($debug) echo "overall ccount ".$this->comments_count."<br />";
		$this->all_comments = array_merge($comments, $this->all_comments);
		$this->level++;

		//		echo "ccount".count($comments)."<br />";

		if ($this->comments_count + $next_level_c_count < M) {
			if ($debug) echo "2<br/>";
			// кол-во всех записей не превысит M, если мы выведем следующий уровень
			$comm_oids = NULL;

			foreach ($comments as $o=>$p) $comm_oids[] = $p['oid']; // накапливаем какие именно комменты первого уровня нужно "раскрыть"

			$this->getComments_Recursive(NULL, $comm_oids);

			// запрашиваем следующий уровень
		} elseif ($this->comments_count < M) { // здесь мы понимаем что новый виток мы будем делать, но догружать будем точно не все
			if ($debug) echo "3<br/>";
			// пройти весь массив и суммируя кол-во комментов до тех пор пока оно не достингет M,
			// собрать массив Oid комментариев для запроса по второму уровню
			$ccount = 0;
			reset($comments);
			$comm_oids = NULL;

			while ( list($k, $v) = each($comments) ) {

				if ($this->comments_count + $ccount < M) {
					// накапливаем до тех пор, пока кол-во комментов не превысит допустимое
					$ccount += $v['l_count'];
					$comm_oids[] = $v['oid'];
				} else {
					// остальное получается не будет догружено - поставим метку
					$this->not_loaded[ $v['oid'] ] = 1;
				}
			}
			$this->getComments_Recursive(NULL, $comm_oids);
		} else {
			if ($debug) echo "4<br />";

			// не будем больше ничего запрашивать, и так слишком много уже
			// запомним про все что запросили, что оно содержит не догруженных детей
			foreach ($comments as $k=>$v) $this->not_loaded[ $v['oid'] ] = 1;

		}

	}

	private function getCommentsOnLevel( $parent_oid, $level, $limit_from=NULL, $limit_cnt=NULL, $comm_oids=NULL ) {
		$db = $this->db;
		$prefix = $db->db_prefix;

		// соберем sql для уровней
		$level_sql = "LEFT OUTER JOIN t_acl_tree tat1 ON tat1.parent_id = pnt.id";

		if ($level>1) {
			for ($i=2; $i<=$level; $i++) {
				$level_sql .= " LEFT OUTER JOIN t_acl_tree tat{$i} ON tat{$i}.parent_id = tat".($i-1).".id ";
			}
		}

		$prev_level = $level - 1;

		if ( !empty($comm_oids) ) {
			foreach ($comm_oids as $k=>$v) {
				$s[] = "tat{$prev_level}.id_object = ".$v." ";
			}
			$where_oids = implode(" OR ", $s);
		}

		$sql = "
		SELECT {$prefix}objects.*, ".($level>1 ? "tat{$prev_level}.id_object AS parent_oid":"pnt.id_object AS parent_oid").",
		(
		SELECT COUNT(node.id) FROM t_acl_tree as node, t_acl_tree as parent
		WHERE parent.id_object = tat{$level}.id_object AND node.parent_id = parent.id
		) AS l_count,
		user.fullname,
		user.avatar
		FROM t_acl_tree AS pnt
		".$level_sql."
		LEFT OUTER JOIN {$prefix}objects ON tat{$level}.id_object = {$prefix}objects.oid
		LEFT OUTER JOIN {$prefix}class_comment cm ON cm.oid = {$prefix}objects.oid
		LEFT OUTER JOIN {$prefix}class_user user ON user.oid = cm.posted_by_id
		WHERE pnt.id_object = ".$parent_oid." AND {$prefix}objects.oid IS NOT NULL ".($where_oids?"AND (".$where_oids.")":"")."
		ORDER BY ctime ".ORDERING." ".(isset($limit_from) ? "LIMIT {$limit_from},{$limit_cnt}" : "");

		$rs2 = $this->store->findSQL($sql);

		// приводим к нужной структуре данных
		if ($rs2)
		while ( $comment = $rs2->next() ) {
			$row = $rs2->rs->current();
			$_comment = array_merge($comment->data, $row);
			if (!is_numeric($_comment['offset'])) { $_comment['offset'] = 1; }

			$_comment['ctime'] = RL_CyrDate::newFromStr("%Y-%m-%d %H:%i:%s", $comment->ctime);

			// проверим были ли изменения коммента?
			if ($_comment['edited_user_oid'] > 0) {
				if ( $this->mm ) {
					if (!$this->m_user) $this->m_user = $this->mm->getModel("User", "m");

					$edited_user = $this->m_user->getUser( $_comment['edited_user_oid'] );
					$_comment['edited_user'] = $edited_user->data;
					$_comment['edited_rlcd'] = RL_CyrDate::newFromStr("%Y-%m-%d %H:%i:%s", $_comment['edited_dtime']);
				}
			}

			$comments[] = $_comment;
			$next_level_c_count += $_comment['l_count'];
		}

		return array($comments, $next_level_c_count);

	}

	private function getCommentsCountLevelOne( $parent_oid ) {
		$prefix = $this->db->db_prefix;
		$sql = "SELECT COUNT(store_objects.oid) as cnt
		FROM t_acl_tree AS pnt
		LEFT OUTER JOIN t_acl_tree tat1 ON tat1.parent_id = pnt.id
		LEFT OUTER JOIN {$prefix}objects ON tat1.id_object = {$prefix}objects.oid
		WHERE pnt.id_object = ".$parent_oid;
		$count = $this->db->queryOne($sql);
		return $count['cnt'];
	}

	private function sortCommentsByParent( $_comments ) {

		if (isset($_GET['debug_ajax'])) $debug = true; // включает вывод отладочной информации
		//		$comments = $this->m_comment->getManyCommentsAsObjects( $parent_oid, $this->tdis_enum, $this->user );

		foreach($_comments as $cm_key=>$_comment) {

			if (!isset($_comment['offset'])) $_comment['offset']=1;

			//			if ($_comment['offset']<4) {
			if ($_comment['offset'] > $max_offset) $max_offset = $_comment['offset'];
			$_comments[$cm_key] = $_comment;
			//			}
		}

		//$time = microtime();

		// помним что в массиве все комменты рассортированы по дате
		for($i=2; $i<=$max_offset; $i++) {
			// на каждом комменте рассматриваемого уровня ищем детей и накапливаем в массив для вставки
			if ($debug) echo "<b>entering level ".$i."</b><br />";
			foreach($_comments as $key=>$comment) {

				if ($comment['offset']==$i-1) {
					$level_childs = NULL;

					foreach ($_comments as $k=>$v) {
						//echo $v['parent_oid']. " ".$comment['id']."<br />";
						if ($v['parent_oid']==$comment['oid']) {
							$level_childs[] = $v;

							// попутно удаляем их из главного массива
							//echo "deleting ".$v['id']."<br />";
							$new_key = array_search($v, $_comments);
							array_splice($_comments, $new_key, 1);
							$childs_w_parents[$v['oid']] = 1;
							// если удаленный элемент находится до родителя после которого будет делаться вставка, то уменьшить ключ для вставки на 1
							//		if ($k < $key) $placement_key = $placement_key - 1;
						}

					}

					$placement_key = array_search($comment ,$_comments);
					$childs_w_parents[$comment['oid']] = 1;
					if ($debug) {
						echo "adding childs for ".$comment['oid']."(".$placement_key.") - ".count($level_childs)."<br />";

						echo "was: <br/>";
						foreach ($_comments as $o=>$p) {
							if ($o==$placement_key) echo "v<a href=\"\" title=\"".$p['parent_oid'].":".$p['offset']."\">".$p['oid']."</a>v ";
							else echo "<a href=\"\" title=\"".$p['parent_oid'].":".$p['offset']."\">".$p['oid']."</a> ";

						}
						echo "<br />";

					}


					// делаем вставку
					array_splice($_comments, $placement_key+1, 0, $level_childs);

					if ($debug) {
						echo "child_W_parent: ".$placement_key."<br/>";
						echo "is: <br/>";
						foreach ($_comments as $o=>$p) {
							if ($o==$placement_key) echo "v<a href=\"\" title=\"".$p['parent_oid'].":".$p['offset']."\">".$p['oid']."</a>v ";
							else echo "<a href=\"\" title=\"".$p['parent_oid'].":".$p['offset']."\">".$p['oid']."</a> ";

						}
						echo "<br />";
					}
				}

			}
		}

		// иногда еще бывают дети без родителей, нужно их перенести наверх, предполагая что родитель на предыдущей странице
		foreach ($_comments as $k=>$comment) {
			if (!isset($childs_w_parents[$comment['oid']])) {
				// это без родителей
				$without_parents[$comment['oid']] = $comment;
				array_splice($_comments, array_search($comment, $_comments), 1);
			}
		}
		array_splice($_comments, 0, 0, $without_parents);

		//$time = microtime()-$time;
		//echo number_format($time, 7)."s<br />";
		return $_comments;
	}

}
?>
